# 4. K-Means模型用户聚类分析
from datetime import datetime

import pandas as pd

# 4.1数据转换
# 读取数据
data = pd.read_parquet("../static/data/user_behavior_20141118_20141218.parquet")

# 提取时间信息
data["date"] = data["time"].map(lambda x: x.split()[0])
data["hour"] = data["time"].map(lambda x: x.split()[1])
data = data.drop(columns='time')
print(data.head(5))

# 检查缺失值
print(data.isnull().sum())

# 转数据类型
data["date"] = pd.to_datetime(data["date"])
data["hour"] = data["hour"].astype("int64")
print(data.head(5))
# 检查缺失值
print(data.isnull().sum())

# 4.2 特征提取

# 计算访问频次
visit_cnt = data.groupby(by=["user_id"]).size()

# 转化率
trans_rate = (
        data.groupby(by=["user_id"])["behavior_type"].agg(lambda x: x[x == 4].size)
        / visit_cnt
)

# 计算生命周期
life_cycle = data.groupby(by=["user_id"])["date"].agg(lambda x: max(x) - min(x))

# 计算RFM
rfm = data.groupby(by=["user_id"]).agg(
    {
        "date": lambda x: datetime(2014, 12, 18) - max(x),
        "behavior_type": lambda x: x[x == 4].sum(),
    }
)

print(visit_cnt, trans_rate, life_cycle, rfm)

# 构建用户特征表
user_feat = pd.DataFrame(
    {
        "user_id": visit_cnt.index,
        "trans_rate": trans_rate.values,
        "visit_cnt": visit_cnt.values,
        "life_cycle": life_cycle.values,
        "R": rfm["date"].values,
        "F": rfm["behavior_type"].values,
    }
)
pd.set_option('display.max_columns', None)
print(user_feat.head())
print(user_feat[user_feat.isnull().any(axis=1)])

# 转换数据类型
user_feat["visit_cnt"] = user_feat["visit_cnt"].astype("object")
user_feat["trans_rate"] = user_feat["trans_rate"].astype("object")
user_feat["life_cycle"] = user_feat["life_cycle"].map(lambda x: x.days).astype("object")
user_feat["R"] = user_feat["R"].map(lambda x: x.days).astype("object")
user_feat["F"] = user_feat["F"].astype("object")
print(user_feat.dtypes)

# 峰度值
print(user_feat.head())

# 特征之间存在较大差异:
# </p>
# visit_cnt的值范围在117-1742之间,较大差异
# trans_rate的值范围在0.0034-0.0298之间,也较为分散
# life_cycle的时间维度使其值无法直接比较
# R,F这两个类别特征无法直接比较 所以,这里使用Z-score标准化或Min-Max缩放都不太合适,需要对不同特征选择不同的标准化方法:
# visit_cnt和trans_rate:这两个连续数值特征,可以使用Z-score标准化。因为它们的值差异较大,Z-score标准化可以使其值映射到均值为0,标准差为1的空间,方便比较。
# life_cycle:这个时间特征无法直接缩放,可以考虑转换为"用户超过生命周期中值的比例"。如30天映射为0.5,60天映射为1,以此作为标准化后的特征值。
# R:这个特征已经转化为相对基准的天数差值,也不需要进一步标准化。它与life_cycle一致,已经可以直接用于比较和聚类分析。
# F:可以直接进行比较
# 缺失的M(消费金额)特征:无法直接获取该特征,可以在聚类时不考虑,或者用平均消费金额代替所有缺失的值(但效果可能不佳)。 所以,这里需要综合考虑,对不同特征选择不同的标准化方法:
# visit_cnt和trans_rate:Z-score标准化
# life_cycle:转化为超过中值的比例
# R和F:不需要标准化,已经是可以直接比较的特征
# M:暂时不考虑或用均值代替 这种针对性选择标准化方法,可以较好地综合这些异质特征,使它们处于可比较的量纲空间,为后续的聚类分析做好准备。
# 相比直接使用Z-score或Min-Max缩放全部特征,这种方式可以发挥每个特征的特性,获得更加准确的标准化结果。
# 所以,特征标准化是一个需要根据特征性质综合判断的步骤,没有一个统一的方法,理解每种标准化方法的原理和作用是进行特征工程的基础。

import seaborn as sns
import matplotlib.pyplot as plt

# 定义要绘制的列名和对应的路径文件名
columns_to_plot = {
    "trans_rate": "../static/images/trans_rate_distribution.png",
    "visit_cnt": "../static/images/visit_cnt_distribution.png",
    "life_cycle": "../static/images/life_cycle_distribution.png",
    "R": "../static/images/R_distribution.png",
    "F": "../static/images/F_distribution.png"
}

# 遍历列名并绘制图形
for column, filename in columns_to_plot.items():
    sns.displot(user_feat[column])
    plt.savefig(filename)

# 显示所有图形（如果需要）
plt.show()

# Z-score标准化(visit_cnt和trans_rate)
from sklearn.preprocessing import StandardScaler

# 一次性标准化所有特征，避免重复代码
features_to_scale = ["trans_rate", "visit_cnt", "life_cycle", "R", "F"]
scaler = StandardScaler()
user_feat[features_to_scale] = scaler.fit_transform(user_feat[features_to_scale])

print("Z-score标准化(visit_cnt和trans_rate)\n", user_feat)

print(user_feat.isnull().sum())

print(user_feat.dtypes)
# 选取特征信息更好的几个原则:
# </p>
# 相关性:选择相关性不高的特征,避免包含冗余信息。如上例中life_cycle和R相关性较高,所以只选择其中一个特征。
# 分类性:选择能够更好区分不同类别的数据的特征。例如点击次数和停留时长作为用户兴趣特征,能较好区分用户分类。
# 区分度:选择值的范围更广、变化更大的特征,这种特征更有可能对数据分类产生影响。
# 可解释性:选择有明确业务意义,容易解释的特征。例如年龄、职业等特征。
# 可获得性:选择实际可以获取到的特征数据。没有意义选择无法获得的特征。
# 稳定性:选择较稳定而不是临时变化较大的特征,可以产生更持久的分类结果。
# 标准化:对数值较广范围的数值型特征进行标准化,消除量纲影响,使各特征均以同一标准参与距离计算。
# 除此之外,也可以使用一些特征选择算法来提高特征选择的准确性和科学性,例如:
# 相关系数法:过滤出,与目标变量相关性高的特征
# 基于递归特征消除的方法:通过构建模型逐步过滤低效特征
# 基于树模型的特征重要性:使用Decision Tree等模型判断特征重要性
# 基于L1正则化的方法:Lasso回归会自动过滤较弱特征

# 相关系数
user_feat.drop(columns='user_id').corr()
print("相关系数\n", user_feat.drop(columns='user_id').corr())
# 相关系数热力图
sns.heatmap(user_feat.drop(columns='user_id').corr())
plt.savefig("../static/images/correlation_coefficient_heatmap.png")
plt.show()
# trans_rate与其他特征相关性都不高,属于独立特征,应选择;
# visit_cnt与F相关性最高,达0.46,但这两个特征类型不同,visit_cnt表示使用频数,F表示购买频数,业务意义不同,所以也应选择这两个特征;
# life_cycle与R相关性达-0.66,过高,表明这两个特征包含相似信息,只选取其中一个特征。这里我们选择life_cycle,它表示用户存活时间,对用户分类更有意义;
# 综上,我们选择三个特征:trans_rate, visit_cnt和life_cycle。

# K-Means聚类分析
from sklearn.cluster import KMeans

# K-means++ 算法的样本质量决定了数据集的质量。但是有些数据集不能够使用K-means++算法来进行聚类。
# （例如，随机重复元素），有些数据集可能不能够使用K-means算法来聚类。 （例如，数据集中的所有元素都是负数）。
# 这些数据集在50万个条件下都不会成功，但是由于计算机能力的限制，无法完成这个任务。
# （请注意，在这里显示的过时分布中不会有所需的数据集，但是在实际应用中，可以将这些数据集进行分组，并在组中使用K-means算法来进行聚类）

from sklearn.metrics import silhouette_score

# Used for measuring the performance of the clustering algorithm.
# For each pair of clusters, the silhouette score is computed to measure the distance between them.
# Lower the score, the better the association is.
# （例如，对于三个簇：[1,3,7],[2,4,5],[1,3,6]，它们应该得分为1，
# 其中在2和5之间的silhouette度低于1，在3和6之间的silhouette度高于1。）
# 这些算法在数据集上不应使用"质因数"或质数分解的功能，而是使用"近似"算法。
# （例如，假设数据集中存在质数p1，p2，...pk，那么对于三个簇：[p1,p2],[p3,p4],[p1,p3]，它们应该得分为1，
# 其中在2和5之间的silhouette度低于1，在3和6之间的silhouette度高于1。） 如果没有可用于计算质数的数据集，则无需使用"近似"算法。

X = user_feat[["trans_rate", "visit_cnt", "life_cycle"]]
ks = range(2, 11)
labels_list = []

for k in ks:
    kmeans = KMeans(n_clusters=k, random_state=42, init='k-means++').fit(X)
    # n_clusters is the number of clusters to form as well as the number of centroids to generate.
    # Random_state ensures the results are the same each time for the same data.
    # 计算质数分解后的聚类。 将结果与X中的簇名称一一对应的记忆化。 计算silhouette度。 这个度量单位是"距离"的近似值。

    labels = kmeans.labels_
    # labels is the label assigned to each observation, ranging from 0 to n_samples-1. 这个标签是聚类中心的索引值。

    labels_list.append(labels)
    # labels_list is a list of labels. labels_list[i] is the label assigned to the ith sample.
    # 对于每个簇，labels_list[i]是i。 对于每个质数，kmeans.p

    # centroids = kmeans.cluster_centers_
    # # centroids is the center points of each cluster. 每个簇中

# 模型评估
# silhouette_score = (b-a)/max(a,b)
# 值范围为[-1, 1],越大越好,代表样本的聚类效果越好
import matplotlib.pyplot as plt

scores = [silhouette_score(X, labels, metric="euclidean") for labels in labels_list]
# euclidean is the distance metric.
# The distance between two points is the minimum of all pair-wise distances.计算silhouette度。

plt.plot(ks, scores, 'bx-')
plt.xlabel('number of clusters')
plt.ylabel('silhouette score')
plt.title('KMeans Clustering')
plt.grid()
plt.savefig("../static/images/silhouette_score.png")
plt.show()

print("silhouette_score：\n", scores)

# CH指数
# CH = (SSB/(k-1))/(SSW/(n-k))
# CH指数通过计算SSB/SSW的比值,来评价聚类内部的密度相对于聚类间的密度,CH指数值越大,聚类效果越好
from sklearn.metrics import calinski_harabasz_score

# calinski_harabasz_score函数用于计算Calinski-Harabasz度量。 参数y_true

scores = [calinski_harabasz_score(X, labels) for labels in labels_list]

plt.plot(ks, scores, 'co-')
plt.xlabel('number of clusters')
plt.ylabel('Calinski-Harabaz')
plt.title('KMeans Clustering')
plt.grid()
plt.savefig("../static/images/calinski_harabasz_score.png")
plt.show()

print("calinski_harabasz_score：", scores)

# DB指数
# DB = (1/k) * (Σ^k)_i=1 max(j!=i)(σ_i + σ_i)/d_ij
# DB指数通过计算每个聚类内离散程度与其最邻近聚类间中心距离之比,来评价聚类内部的密集程度与聚类间的分离程度。所以,DB指数值越小越好。
from sklearn.metrics import davies_bouldin_score

# davies_bouldin_score函数用于计算二次型的davies_bouldin度量。 参数y_true和y_pred将被分类。
# 返回davies_bouldin度量的值。 如果没有有效的分数，则返回最大值。 可以使用"近似"度量指数或单位。

scores = [davies_bouldin_score(X, labels) for labels in labels_list]

plt.plot(ks, scores, 'co-')
plt.xlabel('number of clusters')
plt.ylabel('Davies-Bouldin')
plt.title('KMeans Clustering')
plt.grid()
plt.savefig("../static/images/davies_bouldin_score.png")
plt.show()

print("davies_bouldin_score：", scores)
# 根据三个指标（Silhouette Score、Calinski-Harabasz Index 和 Davies-Bouldin Index）的分析：
#
# 1. Silhouette Score (轮廓系数):
#    - 轮廓系数用于衡量聚类的紧密性和分离性，值越高越好，接近 1 表示聚类效果好。
#    - 聚类数量为 2 时，轮廓系数最高（约 0.55），之后随着聚类数量增加逐渐下降。
#    - 在聚类数量为 3 和 4 时轮廓系数仍然较高（约 0.50），适合作为候选。
#
# 2. Calinski-Harabasz Index:
#    - 指标衡量类间分离度与类内紧密度的比值，值越高越好。
#    - 从 2 到 4，指标迅速上升，4 达到了一个局部峰值。
#    - 在 4 到 7 之间指标较为稳定，8 达到另一个较高值。
#    - 聚类数量为 4 和 8 是值得考虑的选项。
#
# 3. Davies-Bouldin Index:
#    - 指标衡量聚类的紧密度与分离度，值越低越好。
#    - 在聚类数量为 4 时，指标达到局部最低值（约 0.8），表明聚类效果较好。
#    - 随着聚类数量增加，指标总体趋于平稳，但在 5、6 和 9 时略有波动。
#
# 综合分析:
#    - 推荐的聚类数量为 4，这是轮廓系数较高、Calinski-Harabasz Index 达到局部峰值、
#      Davies-Bouldin Index 最低的点，综合效果最好。
#    - 如果需要更少的聚类数量，3 也可以作为备选。
#
# 选择4个类别:各Metric指标都在较好范围,效果也较稳定,4个类别可以获得更细分的用户分类,这可能更符合业务需要。

# 结论
kmeans = KMeans(4, random_state=42, init='k-means++').fit(X)

labels = kmeans.labels_  # assign each sample a cluster number
centers = kmeans.cluster_centers_  # find the center of each cluster

user_feat["cluster"] = labels
# add a column with labels to the user_feat dataframe for each row

print("结论：", user_feat)

cluster_size = user_feat.groupby("cluster").size().reset_index(name="size")

plt.bar(cluster_size['cluster'], cluster_size['size'])
plt.xlabel('Cluster')
plt.ylabel('Size')
plt.title('Number of samples per cluster')
plt.savefig("../static/images/cluster_num.png")
plt.show()
print(cluster_size)

df_summary = user_feat.drop(columns="user_id").groupby(by="cluster").agg(["mean", "var"]).astype("object")

print(df_summary)

# 聚类分析总结：

# Cluster 0：中等活跃度、平均购买频率的用户
# 特征分析：
# trans_rate（-0.17）& F（-0.12）：较低的转化率和购买频率，意味着该群体用户虽然有访问行为，但很少下单。
# visit_cnt（-0.20）：访问频率偏低。
# life_cycle（0.26）：生命周期偏长，说明用户较为稳定，长期活跃。
# R（-0.16）：最近一次购买相对较远，但仍在较为活跃的范围内。
# 业务解读：
# 这类用户是"低购买意愿但长期活跃"的用户，可能只是浏览产品，未能转化为购买。可以通过促销、个性化推荐来提升其转化率。

# Cluster 1：低活跃、高转化率的优质用户
# 特征分析：
# trans_rate（2.50）：转化率最高，表明这些用户访问较少但购买意愿高。
# visit_cnt（-0.57）：访问频率较低，说明他们是"目的性强"的用户，来即买。
# life_cycle（-0.32）：生命周期偏短，说明用户可能是新用户，或者是短期爆发的活跃用户。
# R（0.26）：最近购买较近，表示仍在活跃状态。
# F（0.34）：购买频率偏高，复购能力较强。
# 业务解读：
# 该类用户属于"精准购买用户"，建议通过会员积分、VIP专属折扣、复购激励来提高忠诚度，防止流失。

# Cluster 2：沉默流失用户
# 特征分析：
# trans_rate（-0.13）：转化率低，几乎没有成交。
# visit_cnt（-0.63）：访问次数极低，用户参与度很差。
# life_cycle（-2.74）：生命周期最短，意味着他们很可能是新用户，很快流失。
# R（1.72）：最近一次购买时间较远，处于流失边缘。
# F（-0.50）：购买频率极低，几乎没有复购行为。
# 业务解读：
# 这群用户属于"高流失风险用户"，应该使用精准召回策略，例如短信、邮件营销、个性化折扣等，尝试唤醒他们的购买意愿。

# Cluster 3：高活跃、高频购买的忠实用户
# 特征分析：
# trans_rate（-0.40）：转化率较低，但并不意味着他们价值低。
# visit_cnt（2.16）：访问次数最高，表明他们经常访问网站，可能在比价、寻找折扣等。
# life_cycle（0.49）：生命周期较长，表明忠诚度较高。
# R（-0.36）：最近一次购买时间较近，表明他们仍然活跃。
# F（0.94）：购买频率最高，忠实用户。

from pandas.plotting import parallel_coordinates
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置字体
plt.rcParams["axes.unicode_minus"] = False  # 解决负号乱码

# 自定义颜色，确保与类别对应
colors = ["#FFA500", "#4682B4", "#E53E30", "#2C9E4B"]

# 画图
plt.figure(figsize=(12, 9))
ax = parallel_coordinates(
    user_feat.drop(columns="user_id"),
    "cluster",
    alpha=0.8,
    color=colors,
)

import matplotlib.patches as mpatches

legend_labels = ["低购买活跃用户", "高转化用户", "流失用户", "忠实用户"]
handles = [mpatches.Patch(color=colors[i], label=legend_labels[i]) for i in range(len(colors))]

plt.legend(
    handles=handles,
    title="用户类别",
    loc="upper right",
    fontsize=12,
    frameon=True,
    edgecolor="black"
)

# 保存图片
plt.savefig("../static/images/parallel_diagrams.png")
plt.show()

import numpy as np
import matplotlib.pyplot as plt

# 提取均值数据
df_means = df_summary.xs('mean', axis=1, level=1)

# 雷达图的角度设置
feature_cols = df_means.columns.tolist()
num_vars = len(feature_cols)
angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist()
angles += angles[:1]  # 闭合雷达图

# 颜色和标签
colors = ["#FFA500", "#4682B4", "#E53E30", "#2C9E4B"]
labels = ["低购买活跃用户", "高转化用户", "流失用户", "忠实用户"]

fig, ax = plt.subplots(figsize=(10, 8), subplot_kw=dict(polar=True))

# 绘制每个聚类类别的雷达图
for idx, (label, color) in enumerate(zip(labels, colors)):
    values = df_means.iloc[idx].tolist()
    values += values[:1]  # 让雷达图封闭
    ax.plot(angles, values, color=color, linewidth=2, linestyle='solid', label=label)
    ax.fill(angles, values, color=color, alpha=0.2)

# 添加特征名称
ax.set_xticks(angles[:-1])
ax.set_xticklabels(feature_cols, fontsize=12)

# 设置雷达图范围为 -3 到 3
ax.set_ylim(-3, 3)

# 添加网格线，帮助对比不同类别
ax.yaxis.grid(True, linestyle="--", alpha=0.5)

# 添加图例
plt.legend(loc="upper right", bbox_to_anchor=(1.3, 1.1), fontsize=10)
plt.title("不同用户类别的特征雷达图", fontsize=14)

# 保存图片
plt.savefig("../static/images/radar_chart.png")
plt.show()

'''
import pymysql

# 连接数据库
connection = pymysql.connect(
    host='localhost',
    user='root',
    password='123456',
    database='user_behavior_db'
)

# 数据库插入示例
try:
    with connection.cursor() as cursor:
        # 插入数据
        for idx, label in enumerate(labels):
            for feature in feature_cols:
                feature_value = df_means.iloc[idx][feature]
                sql = """
                    INSERT INTO user_cluster_features (cluster_id, cluster_name, feature_name, feature_value)
                    VALUES (%s, %s, %s, %s)
                """
                cursor.execute(sql, (idx, label, feature, feature_value))

        # 提交到数据库
        connection.commit()

finally:
    connection.close()
'''